﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace DLR03
{
    /// <summary>
    /// This application contains the minimum functionality for an expression to interact with
    /// a scope.  In this case two input values (n1 and n2) are placed in the scope, then the
    /// expression is executed which places a new value (result) in the scope.
    /// </summary>
    public class Program
    {
        public static void Main(string[] args)
        {
            var scope = new Scope();
            var simple = new Simple();

            scope["n1"] = 5;
            scope["n2"] = 7;
            simple.Run(scope);

            Console.WriteLine("Scope dump:");
            foreach (var item in scope)
            {
                Console.WriteLine("{0} = {1}", item.Name, item.Data);
            }
        }
    }

    /// <summary>
    /// A Scope object enumerates the symbols availabe to the executing expression.
    /// </summary>
    /// <remarks>
    /// The GetSymbol method is used by the ExpressionEx.SymbolExpression method to
    /// access a symbol.  The indexer is a convenience tool that the programmer can
    /// use to get and set symbol data.
    /// </remarks>
    public class Scope : IEnumerable<Symbol>
    {
        #region Variables

        private List<Symbol> _list;

        #endregion

        #region Constructors

        public Scope()
        {
            _list = new List<Symbol>();
        }

        #endregion

        #region Properties

        public dynamic this[string name]
        {
            get
            {
                return GetSymbol(name).Data;
            }
            set
            {
                EnsureThatSymbolExists(name);
                _list.Where(x => x.Name == name).First().Data = value;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// This is used by ExpressionEx.SymbolExpression to retrieve a symbol.
        /// </summary>
        public Symbol GetSymbol(string name)
        {
            EnsureThatSymbolExists(name);
            return _list.Where(x => x.Name == name).First();
        }

        public IEnumerator<Symbol> GetEnumerator()
        {
            return _list.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _list.GetEnumerator();
        }

        private void EnsureThatSymbolExists(string symbolName)
        {
            if (!_list.Any(x => x.Name == symbolName))
            {
                _list.Add(new Symbol() { Name = symbolName, Data = null });
            }
        }

        #endregion
    }

    /// <summary>
    /// Represents a single item in the scope.
    /// </summary>
    /// <remarks>
    /// A variable is referred to by a string identifier, and can contain anything.
    /// </remarks>
    public class Symbol
    {
        public string Name { get; set; }
        public dynamic Data { get; set; }
    }

    public class Simple
    {
        private Dictionary<string, object> _symbols;

        private ParameterExpression _scope;

        public Simple()
        {
            _symbols = new Dictionary<string, object>();
            _scope = Expression.Parameter(typeof(Scope), "scope");
        }

        /// <remarks>
        /// Symbol.Data is dynamic, so the addition operation must also be dynamic.
        /// This requires a new BinaryOperationBinder.  The DynamicAdd and
        /// SymbolExpression methods have been created to make the code easier to read.
        /// </remarks>
        public object Run(Scope scope)
        {
            var addBody =
                Expression.Assign(
                    ExpressionEx.SymbolExpression(_scope, "result"),
                    ExpressionEx.DynamicAdd(
                        ExpressionEx.SymbolExpression(_scope, "n1"),
                        ExpressionEx.SymbolExpression(_scope, "n2")
                    )
                );
            
            var function = Expression.Lambda<Func<Scope, object>>(addBody, _scope);
            var compiled = function.Compile();
            return compiled(scope); // execute the code in the current scope
        }
    }

    /// <summary>
    /// A dynamic expression must have a DynamicMetaObjectBinder class to act on.
    /// This class ensures that two dynamic values can be added together, and
    /// creates the result.
    /// </summary>
    public class SimpleBinaryOperationBinder : BinaryOperationBinder
    {
        public SimpleBinaryOperationBinder(ExpressionType expressionType)
            : base(expressionType)
        {
        }

        /// <summary>
        /// This is called to make sure the two dynamic values are added together somehow.
        /// </summary>
        public override DynamicMetaObject FallbackBinaryOperation(DynamicMetaObject target, DynamicMetaObject arg, DynamicMetaObject errorSuggestion)
        {
            if (!target.HasValue || !arg.HasValue)
            {
                return Defer(target, arg);
            }
            
            var restrictions = target.Restrictions
                .Merge(arg.Restrictions)
                .Merge(BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType))
                .Merge(BindingRestrictions.GetTypeRestriction(arg.Expression, arg.LimitType));
            
            return new DynamicMetaObject(
                ExpressionEx.EnsureObjectResult(Expression.MakeBinary(
                    Operation,
                    Expression.Convert(target.Expression, target.LimitType),
                    Expression.Convert(arg.Expression, arg.LimitType)
                )),
                restrictions
            );
        }
    }

    public static class ExpressionEx
    {
        /// <summary>
        /// Add two dynamic values together.
        /// </summary>
        public static Expression DynamicAdd(Expression left, Expression right)
        {
            return Expression.Dynamic(
                        new SimpleBinaryOperationBinder(ExpressionType.Add),
                        typeof(object),
                        left,
                        right);
        }

        /// <summary>
        /// Retrieve a variable from the input scope by name.
        /// </summary>
        /// <param name="scope">An expression which refers to an object of type Scope.</param>
        /// <param name="symbolName">A symbol name within the given Scope object.</param>
        public static Expression SymbolExpression(Expression scope, string symbolName)
        {
            if (scope.Type != typeof(Scope))
            {
                throw new ArgumentException("The scope expression must represent a Scope object.");
            }
            return Expression.Property(Expression.Call(scope, "GetSymbol", null, Expression.Constant(symbolName)), "Data");
        }

        /// <summary>
        /// Ensure that some sort of object type is returned by the expression.
        /// </summary>
        public static Expression EnsureObjectResult(Expression expr)
        {
            if (!expr.Type.IsValueType)
            {
                return expr;
            }
            if (expr.Type == typeof(void))
            {
                return Expression.Block(expr, Expression.Default(typeof(object)));
            }
            else
            {
                return Expression.Convert(expr, typeof(object));
            }
        }
    }
}